/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.image.ColorModel; // 3-22-00 used in deprecated methods only
import java.awt.image.IndexColorModel; // 3-22-00 used in deprecated mthds only
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel; // 3-22-00 used in deprecated methods only
import java.awt.image.WritableRaster;
import java.util.Map;
import java.util.Vector;
import org.eclipse.imagen.media.util.ImageUtil;
import org.eclipse.imagen.media.util.JDKWorkarounds;

/**
 * This is the base class for all image operations. It provides a home for information and functionalities common to all
 * the op-image classes, and implements various utility methods that may be useful to a specific operation. Image
 * operations may be divided into different categories based on their characteristics. A subclass, extending <code>
 * OpImage</code>, represents a category and implements methods unique and common to those operations. Each individual
 * operator should extend the subclass that represents the specific category that operator belongs to.
 *
 * <p>The layout variables of an <code>OpImage</code> are inherited from the <code>PlanarImage</code> superclass. The
 * layout should be set when the <code>OpImage</code> is constructed. Each subclass must set the appropriate layout
 * variables and supply them via the <code>ImageLayout</code> argument at construction time. This class simply modifies
 * these settings as described in the <code>OpImage</code> constructor comments before forwarding the layout to the
 * <code>PlanarImage</code> constructor. If a subclass needs to modify any of the layout settings subsequent to invoking
 * its superclass constructors it should use the <code>setImageLayout()</code> method defined in <code>PlanarImage
 * </code> in preference to setting the layout variables directly.
 *
 * <p>A <code>RenderedImage</code>'s pixel data type and number of bands are defined by its <code>SampleModel</code>,
 * while the <code>ColorModel</code> translates the pixel data into color/alpha components in the specific <code>
 * ColorSpace</code> that is associated with the <code>ColorModel</code>.
 *
 * <p>By default, the operators provided by Java Advanced Imaging (JAI) operate on the image's pixel data only. That is,
 * the computations are performed on the data described by the image's <code>SampleModel</code>. No color translation is
 * performed prior to the actual computation by the operator, regardless of the type of the <code>ColorModel</code> an
 * image has. If a user intends to have an operation performed on the color data, he must perform the color translation
 * explicitly prior to invoking the operation.
 *
 * <p>There are those operators that specifically deal with the color/alpha data of an image. Such an operator must
 * state its behavior in its <code>OperationDescriptor</code> explicitly and explain its intended usage of the image's
 * color/alpha component data. In such cases, the image's <code>ColorModel</code> as well as the associated <code>
 * ColorSpace</code> should be considered.
 *
 * <p>However there are certain operations, the results of which are incorrect when the source has colormapped imagery,
 * i.e. the source has an <code>IndexColorModel</code>, and the computations are performed on the image's non color
 * transformed pixel data. In JAI, such operations are those that are implemented as subclasses of {@link AreaOpImage},
 * {@link GeometricOpImage}, and the "format" operation. These operations set the
 * {@link JAI#KEY_REPLACE_INDEX_COLOR_MODEL} <code>RenderingHint</code> to true, thus ensuring that the operations are
 * performed correctly on the colormapped imagery, not treating the indices into the color map as pixel data.
 *
 * <p>The tile cache and scheduler are handled by this class. JAI provides a default implementation for <code>TileCache
 * </code> and <code>TileScheduler</code>. However, they may be overriden by each application. An <code>OpImage</code>
 * may share a common cache with other <code>OpImage</code>s, or it may have a private cache of its own. To override an
 * existing cache, use the <code>setTileCache</code> method; an input argument of <code>null</code> indicates that this
 * image should not have a tile cache.
 *
 * <p>The <code>getTile</code> method may be used to request a tile of the image. The default implementation of this
 * method in this class first checks whether the requested tile is in the tile cache, and if not, uses the default
 * <code>TileScheduler</code> to schedule the tile for computation. Once the tile has been computed, it is added to the
 * cache and returned as a <code>Raster</code>.
 *
 * <p>The JAI tile scheduler assumes that when a request is made to schedule a tile for computation via the <code>
 * scheduleTile</code> method, that tile is not currently in the cache. To avoid a cycle, it calls <code>
 * OpImage.computeTile</code> for the actual tile computation.
 *
 * <p>The default implementation of the <code>computeTile</code> method in this class first creates a new <code>Raster
 * </code> to represent the requested tile, then calls one of the two <code>computeRect</code> methods to compute the
 * actual pixel values and store the result in the <code>DataBuffer</code> of the <code>Raster</code>.
 *
 * <p>Two variants of the <code>computeRect</code> method exist.
 *
 * <p>The first (with input arguments <code>Raster[]</code>, <code>WritableRaster</code>, and <code>Rectangle</code>) is
 * used when the <code>OpImage</code> is constructed with the <code>cobbleSources</code> argument set to <code>true
 * </code>. This indicates that the source data must be cobbled into a single <code>Raster</code> and that all the
 * necessary source data are provided in order to compute the rectangular region of the destination image. The source
 * <code>Raster</code> array contains one entry for each source image.
 *
 * <p>The second (with input arguments <code>PlanarImage[]</code>, <code>WritableRaster</code>, and <code>Rectangle
 * </code>) is used when the <code>OpImage</code> is constructed with the <code>cobbleSources</code> argument set to
 * <code>false</code>. This indicates that the source data are not cobbled into a single <code>Raster</code>; instead an
 * array of <code>PlanarImage</code>s, one for each source, supply the source data and each image is responsible for
 * performing its own data accesses. This variant is generally useful if iterators are to be used for the underlying
 * implementation of accessing the image data.
 *
 * <p>The two <code>computeRect</code> methods are not abstract because normally only one needs to be implemented by the
 * subclass depending on the <code>cobbleSources</code> value. The default implementation of these two methods in this
 * class throws a <code>RuntimeException</code>.
 *
 * <p>Every operator who follows the above default implementation must supply an overridden version of at least one of
 * the <code>computeRect</code> method variants, and specify which one is to be called via the <code>cobbleSources
 * </code> argument of the constructor, or an exception will be thrown at run time.
 *
 * <p>If a subclass overrides <code>getTile</code> not to call <code>computeTile</code>, does not use the JAI
 * implementation of <code>TileScheduler</code>, overrides <code>computeTile</code> not to call <code>computeRect</code>
 * , or does not follow the above default implementation in any way, then it may need to handle issues such as tile
 * caching, multi-threading, and etc. by itself and may not need to override some of the methods described above. In
 * some cases, some of the methods or variables are even irrelevant. However, subclasses should be careful when not
 * following the default path for computing a tile. Most importantly, when a subclass overrides <code>getTile</code>, it
 * should also override <code>computeTile</code>.
 *
 * <p>To request multiple tiles at a time, it is preferable to call the <code>getTiles</code> method with a complete
 * list of the requested tiles' indices, than to call <code>getTile</code> once per tile. The implementation of <code>
 * getTiles</code> in this class is optimized using multi-threading so that multiple tiles are computed simultaneously.
 *
 * @see PlanarImage
 * @see AreaOpImage
 * @see GeometricOpImage
 * @see PointOpImage
 * @see StatisticsOpImage
 * @see SourcelessOpImage
 */
public abstract class OpImage extends PlanarImage {

    /** A constant indicating that an operation is likely to spend its time mainly performing computation. */
    public static final int OP_COMPUTE_BOUND = 1;

    /** A constant indicating that an operation is likely to spend its time mainly performing local I/O. */
    public static final int OP_IO_BOUND = 2;

    /** A constant indicating that an operation is likely to spend its time mainly performing network I/O. */
    public static final int OP_NETWORK_BOUND = 3;

    /** A constant equal to what would be returned by <code>ImageLayout.getValidMask()</code> if all fields were set. */
    private static final int LAYOUT_MASK_ALL = ImageLayout.MIN_X_MASK
            | ImageLayout.MIN_Y_MASK
            | ImageLayout.WIDTH_MASK
            | ImageLayout.HEIGHT_MASK
            | ImageLayout.TILE_GRID_X_OFFSET_MASK
            | ImageLayout.TILE_GRID_Y_OFFSET_MASK
            | ImageLayout.TILE_WIDTH_MASK
            | ImageLayout.TILE_HEIGHT_MASK
            | ImageLayout.SAMPLE_MODEL_MASK
            | ImageLayout.COLOR_MODEL_MASK;

    /**
     * The cache object used to cache this image's tiles. It may refer to a common cache shared by many <code>OpImage
     * </code>s or a private cache for this image only. If it is <code>null</code>, it indicates that this image does
     * not have a tile cache.
     */
    protected transient TileCache cache;

    /**
     * Metric used to produce an ordered list of tiles. This determines which tiles are removed from the cache first if
     * a memory control operation is required.
     *
     * @since JAI 1.1
     */
    protected Object tileCacheMetric;

    /** The scheduler to be used to schedule tile computation. */
    private transient TileScheduler scheduler = JAI.getDefaultInstance().getTileScheduler();

    /** Variable indicating whether the TileScheduler is the Sun implementation. */
    private boolean isSunTileScheduler = false;

    /**
     * Indicates which one of the two <code>computeRect</code> variants should be called by the <code>computeTile</code>
     * method. If it is <code>true</code>, <code>computeRect</code> expects contiguous sources.
     */
    protected boolean cobbleSources;

    /** Whether dispose() has been invoked. */
    private boolean isDisposed = false;

    /** Flag indicating that tile recycling is enabled for tiles which may be referenced outside the API. */
    private boolean isCachedTileRecyclingEnabled = false;

    /**
     * A <code>TileRecycler</code> for use in <code>createTile()</code>. May be <code>null</code>. This field is set by
     * the configuration map passed to {@link #OpImage(Vector,ImageLayout,Map,boolean}.
     *
     * @since JAI 1.1.2
     */
    protected TileRecycler tileRecycler;

    /** The default RasterAccessor format tags. */
    // XXX This variable should be removed if we stop using RasterAccessor.
    private RasterFormatTag[] formatTags = null;

    /**
     * Creates a new <code>ImageLayout</code> or forwards the argument layout with or without modifications.
     *
     * <p>If the <code>layout</code> parameter is non-<code>null</code> and all its fields are set then it is cloned and
     * returned.
     *
     * <p>If the <code>layout</code> parameter is non-<code>null</code> but some of its fields are not set and there is
     * at least one source available, then all fields of the layout which are not set are copied from the corresponding
     * attributes of the first source except possibly the <code>ColorModel</code>. The <code>ColorModel</code> is copied
     * if and only if the destination <code>SampleModel</code> is non-<code>null</code> and the <code>ColorModel</code>
     * and <code>SampleModel</code> are compatible.
     *
     * <p>The image's tile dimensions will be set by the first applicable means in the following priority-ordered list.
     * Note that each tile dimension, the <code>tileWidth</code> and the <code>tileHeight</code>, is considered
     * independently :
     *
     * <ol>
     *   <li>Tile dimension set in the <code>ImageLayout</code> (either by the user or the operator itself);
     *   <li>Tile dimension of source, if source is non-<code>null</code>. The tile dimension will be clamped to the
     *       minimum of that of the source tile dimension and the image's corresponding dimension;
     *   <li>Non-<code>null</code> default tile size returned by <code>JAI.getDefaultTileSize()</code>, if the
     *       corresponding image dimension is at least double the default tile size;
     *   <li>The dimensions of the image itself;
     * </ol>
     *
     * <p>If the <code>layout</code> parameter is <code>null</code> and there is at least one source available, a new
     * layout is created from the first source and returned directly.
     */
    private static ImageLayout layoutHelper(ImageLayout layout, Vector sources, Map config) {
        // Initialize to a reference to the layout passed in.
        ImageLayout il = layout;

        // Check the source Vector elements for nulls.
        if (sources != null) {
            checkSourceVector(sources, true);
        }

        // Check the class of the first source to avoid an exception here; a
        // class cast exception will be thrown by the PlanarImage constructor.
        RenderedImage im = sources != null && sources.size() > 0 && sources.firstElement() instanceof RenderedImage
                ? (RenderedImage) sources.firstElement()
                : null;

        // Update some or all of the layout if a source is available.
        if (im != null) {
            // Create a new layout with the source as fallback.
            // The ColorModel field is intentionally NOT set.
            if (layout == null) {
                // Copy entirety of source image layout.
                il = layout = new ImageLayout(im);

                // Invalidate the ColorModel setting.
                il.unsetValid(ImageLayout.COLOR_MODEL_MASK);
            } else {
                // Set all fields except ColorModel.
                il = new ImageLayout(
                        layout.getMinX(im),
                        layout.getMinY(im),
                        layout.getWidth(im),
                        layout.getHeight(im),
                        layout.getTileGridXOffset(im),
                        layout.getTileGridYOffset(im),
                        layout.getTileWidth(im),
                        layout.getTileHeight(im),
                        layout.getSampleModel(im),
                        null);
            }

            // At this point "layout" and "il" are non-null with "layout"
            // representing the ImageLayout originally passed in.  "il" does
            // not yet have its ColorModel field set.

            // Set the ColorModel.
            if (layout.isValid(ImageLayout.COLOR_MODEL_MASK) && layout.getColorModel(null) == null) {
                // User wants to force a null ColorModel.
                il.setColorModel(null);

            } else if (il.getSampleModel(null) != null) {

                // Target SampleModel is available.

                // Get SampleModel from "il"; guaranteed to be non-null.
                SampleModel sm = il.getSampleModel(null);

                // Get ColorModel from "layout".
                ColorModel cmLayout = layout.getColorModel(null);

                // First attempt to set the ColorModel to that specified
                // in the original layout, if any.
                if (cmLayout != null) {
                    // ColorModel is set in original layout.
                    if (JDKWorkarounds.areCompatibleDataModels(sm, cmLayout)) {
                        // ColorModel is compatible with target SampleModel.
                        il.setColorModel(cmLayout);

                    } else if (layout.getSampleModel(null) == null) {
                        // SampleModel not set in original layout so
                        // ColorModel must be incompatible with source
                        // SampleModel: create a compatible SampleModel.

                        // Set the ColorModel to that desired.
                        il.setColorModel(cmLayout);

                        // Derive a new SampleModel from the desired ColorModel
                        SampleModel derivedSM =
                                cmLayout.createCompatibleSampleModel(il.getTileWidth(null), il.getTileHeight(null));

                        // Set the SampleModel to that derived from the CM.
                        il.setSampleModel(derivedSM);
                    }
                }

                // If ColorModel not set from ImageLayout, attempt to set
                // using a ColorModelFactory and if that fails, attempt to
                // use the ColorModel of the source.
                if (!il.isValid(ImageLayout.COLOR_MODEL_MASK) && !setColorModelFromFactory(sm, sources, config, il)) {
                    // Get ColorModel from "im", i.e., the source.
                    ColorModel cmSource = im.getColorModel();
                    if (cmSource != null && JDKWorkarounds.areCompatibleDataModels(sm, cmSource)) {
                        // Set to source ColorModel.
                        if (cmSource != null
                                && cmSource instanceof IndexColorModel
                                && config != null
                                && config.containsKey(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)
                                && ((Boolean) config.get(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)).booleanValue()) {

                            ColorModel newCM =
                                    PlanarImage.getDefaultColorModel(sm.getDataType(), cmSource.getNumComponents());

                            SampleModel newSM;
                            if (newCM != null) {
                                newSM = newCM.createCompatibleSampleModel(
                                        il.getTileWidth(null), il.getTileHeight(null));
                            } else {
                                newSM = RasterFactory.createPixelInterleavedSampleModel(
                                        sm.getDataType(),
                                        il.getTileWidth(null),
                                        il.getTileHeight(null),
                                        cmSource.getNumComponents());
                            }

                            il.setSampleModel(newSM);
                            if (newCM != null) il.setColorModel(newCM);
                        } else {
                            il.setColorModel(cmSource);
                        }
                    }
                }
            } else if (il.getSampleModel(null) == null) { // null SampleModel
                // Set to whatever is available.
                il.setColorModel(layout.getColorModel(im));
            }
            // end of block if(im != null)
        } else if (il != null) {
            // Can only get here if im == null && layout != null.
            // Make sure that il is a clone of layout.
            il = (ImageLayout) layout.clone();

            // If the ColorModel is set but the SampleModel is not,
            // derive a SampleModel from the ColorModel.
            if (il.getColorModel(null) != null && il.getSampleModel(null) == null) {
                // Set SampleModel dimensions.
                int smWidth = il.getTileWidth(null);
                if (smWidth == 0) {
                    smWidth = 512;
                }
                int smHeight = il.getTileHeight(null);
                if (smHeight == 0) {
                    smHeight = 512;
                }

                // Derive a new SampleModel from the desired ColorModel
                SampleModel derivedSM = il.getColorModel(null).createCompatibleSampleModel(smWidth, smHeight);

                // Set the SampleModel to that derived from the CM.
                il.setSampleModel(derivedSM);
            }
        } // end of block if(il != null)

        // If no ColorModel is set, then first attempt to set a ColorModel
        // using the ColorModelFactory; otherwise set a default ColorModel
        // if either the configuration mapping is null, it does not contain
        // a mapping of the key KEY_DEFAULT_COLOR_MODEL_ENABLED, or this key
        // is mapped to Boolean.TRUE.
        if (il != null
                && !il.isValid(ImageLayout.COLOR_MODEL_MASK)
                && il.getSampleModel(null) != null
                && !setColorModelFromFactory(il.getSampleModel(null), sources, config, il)) {

            ColorModel cm = null;
            SampleModel srcSM = il.getSampleModel(null);

            // If it is not a byte image, then probably all the above did not
            // manage to set a ColorModel and ImageUtil.getCompatibleColorModel
            // will be used to get the ColorModel. However we want to ensure
            // that the destination ColorModel is expanded if the ColorModel
            // of the source is an IndexColorModel and we've been asked to
            // do IndexColorModel expansion
            if (im != null
                    && im.getColorModel() != null
                    && im.getColorModel() instanceof IndexColorModel
                    && config != null
                    && config.containsKey(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)
                    && ((Boolean) config.get(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)).booleanValue()) {

                IndexColorModel icm = (IndexColorModel) im.getColorModel();

                // We need to change the ColorModel to a non IndexColorModel
                // so that operations such as geometric can take place
                // correctly. If the ColorModel is changed the SampleModel
                // needs to be changed too, so that they are compatible
                cm = PlanarImage.getDefaultColorModel(srcSM.getDataType(), icm.getNumComponents());

                SampleModel newSM;
                if (cm != null) {
                    newSM = cm.createCompatibleSampleModel(srcSM.getWidth(), srcSM.getHeight());
                } else {
                    newSM = RasterFactory.createPixelInterleavedSampleModel(
                            srcSM.getDataType(), srcSM.getWidth(), srcSM.getHeight(), icm.getNumComponents());
                }

                il.setSampleModel(newSM);

            } else {

                cm = ImageUtil.getCompatibleColorModel(il.getSampleModel(null), config);
            }

            // Set ColorModel only if method succeeded.
            if (cm != null) il.setColorModel(cm);
        }

        // If the tile dimensions were not set in "layout" and are either
        // not set in "il" or would yield an untiled image then reset them to
        // the global tile size default if each image dimension is at least
        // double the respective default tile dimension.
        if (layout != null
                && il != null
                && !layout.isValid(ImageLayout.TILE_WIDTH_MASK | ImageLayout.TILE_HEIGHT_MASK)) {
            Dimension defaultTileSize = JAI.getDefaultTileSize();
            if (defaultTileSize != null) {
                if (!layout.isValid(ImageLayout.TILE_WIDTH_MASK)) {
                    if (il.getTileWidth(null) <= 0) {
                        il.setTileWidth(defaultTileSize.width);
                    } else {
                        // Calculate number of tiles along X.
                        int numX = XToTileX(
                                        il.getMinX(null) + il.getWidth(null) - 1,
                                        il.getTileGridXOffset(null),
                                        il.getTileWidth(null))
                                - XToTileX(il.getMinX(null), il.getTileGridXOffset(null), il.getTileWidth(null))
                                + 1;

                        // Reset if single-tiled and image is big enough in X.
                        if (numX <= 1 && il.getWidth(null) >= 2 * defaultTileSize.width) {
                            il.setTileWidth(defaultTileSize.width);
                        }
                    }
                }

                if (!layout.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
                    if (il.getTileHeight(null) <= 0) {
                        il.setTileHeight(defaultTileSize.height);
                    } else {
                        // Calculate number of tiles along Y.
                        int numY = YToTileY(
                                        il.getMinY(null) + il.getHeight(null) - 1,
                                        il.getTileGridYOffset(null),
                                        il.getTileHeight(null))
                                - YToTileY(il.getMinY(null), il.getTileGridYOffset(null), il.getTileHeight(null))
                                + 1;

                        // Reset if single-tiled and image is big enough in Y.
                        if (numY <= 1 && il.getHeight(null) >= 2 * defaultTileSize.height) {
                            il.setTileHeight(defaultTileSize.height);
                        }
                    }
                }
            }
        }

        // Independently clamp each tile dimension to the respective image
        // dimension, if the tile dimensions are not set in any supplied
        // ImageLayout, and the tile and image dimensions are both set in
        // the ImageLayout to be returned.

        // Tile width
        if ((layout == null || !layout.isValid(ImageLayout.TILE_WIDTH_MASK))
                && il.isValid(ImageLayout.TILE_WIDTH_MASK | ImageLayout.WIDTH_MASK)
                && il.getTileWidth(null) > il.getWidth(null)) {
            il.setTileWidth(il.getWidth(null));
        }

        // Tile height
        if ((layout == null || !layout.isValid(ImageLayout.TILE_HEIGHT_MASK))
                && il.isValid(ImageLayout.TILE_HEIGHT_MASK | ImageLayout.HEIGHT_MASK)
                && il.getTileHeight(null) > il.getHeight(null)) {
            il.setTileHeight(il.getHeight(null));
        }

        return il;
    }

    /**
     * Set the <code>ColorModel</code> in <code>layout</code> if <code>config</code> is non-<code>null</code> and
     * contains a mapping for <code>JAI.KEY_COLOR_MODEL_FACTORY</code>. If the <code>ColorModelFactory</code> returns a
     * non-<code>null</code> <code>ColorModel</code> which is compatible with <code>sampleModel</code> it is used to set
     * the <code>ColorModel</code> in <code>layout</code>.
     *
     * @param sampleModel The <code>SampleModel</code> to which the <code>ColorModel</code> to be created must
     *     correspond; may <b>not</b> be <code>null</code>.
     * @param sources A <code>List</code> of <code>RenderedImage</code>s; may be <code>null</code>.
     * @param config A configuration mapping; may be <code>null</code>.
     * @param layout The image layout; will be updated with the <code>ColorModel</code>created by the <code>
     *     ColorModelFactory</code>
     * @return Whether the <code>ColorModel</code> in <code>layout</code> has been set.
     * @exception IllegalArgumentException if <code>sampleModel</code> is <code>null</code>.
     */
    private static boolean setColorModelFromFactory(
            SampleModel sampleModel, Vector sources, Map config, ImageLayout layout) {
        boolean isColorModelSet = false;

        if (config != null && config.containsKey(JAI.KEY_COLOR_MODEL_FACTORY)) {
            ColorModelFactory cmf = (ColorModelFactory) config.get(JAI.KEY_COLOR_MODEL_FACTORY);
            ColorModel cm = cmf.createColorModel(sampleModel, sources, config);
            if (cm != null && JDKWorkarounds.areCompatibleDataModels(sampleModel, cm)) {
                layout.setColorModel(cm);
                isColorModelSet = true;
            }
        }

        return isColorModelSet;
    }

    // XXX Note: ColorModel.isCompatibleRaster() is mentioned below but it
    // should be ColorModel.isCompatibleSampleModel(). This has a bug
    // however (4326636) which is worked around within JAI. The other method
    // is mentioned as it does NOT have a bug.
    /**
     * Constructor.
     *
     * <p>The image's layout is encapsulated in the <code>layout</code> argument. The variables of the image layout
     * which are not set in the <code>layout</code> parameter are copied from the first source if sources are available.
     * In the case of the <code>ColorModel</code>, the copy is performed if and only if the <code>ColorModel</code> is
     * compatible with the destination <code>SampleModel</code> and is not set by another higher priority mechanism as
     * described presently.
     *
     * <p>Assuming that there is at least one source, the image's <code>ColorModel</code> will be set by the first
     * applicable means in the following priority-ordered list:
     *
     * <ol>
     *   <li><code>null</code> <code>ColorModel</code> from <code>ImageLayout</code>;
     *   <li>Non-<code>null</code> <code>ColorModel</code> from <code>ImageLayout</code> if compatible with <code>
     *       SampleModel</code> in <code>ImageLayout</code> or if <code>SampleModel</code> in <code>ImageLayout</code>
     *       is <code>null</code>;
     *   <li>Value returned by <code>ColorModelFactory</code> set via the <code>JAI.KEY_COLOR_MODEL_FACTORY</code>
     *       configuration variable if compatible with <code>SampleModel</code>;
     *   <li>An instance of a non-<code>IndexColorModel</code> (or <code>null</code> if no compatible non-<code>
     *       IndexColorModel</code> could be generated), if the source has an <code>IndexColorModel</code> and <code>
     *       JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> is <code>Boolean.TRUE</code>;
     *   <li><code>ColorModel</code> of first source if compatible with <code>SampleModel</code>;
     *   <li>Value returned by default method specified by the <code>JAI.KEY_DEFAULT_COLOR_MODEL_METHOD</code>
     *       configuration variable if <code>JAI.KEY_DEFAULT_COLOR_MODEL_ENABLED</code> is <code>Boolean.TRUE</code>.
     * </ol>
     *
     * If it is not possible to set the <code>ColorModel</code> by any of these means it will remain <code>null</code>.
     * The image's tile dimensions will be set by the first applicable means in the following priority-ordered list.
     * Note that each tile dimension, the <code>tileWidth</code> and the <code>tileHeight</code>, is considered
     * independently :
     *
     * <ol>
     *   <li>Tile dimension set in the <code>ImageLayout</code> (either by the user or the operator itself);
     *   <li>Tile dimension of source, if source is non-<code>null</code>. The tile dimension will be clamped to the
     *       minimum of that of the source tile dimension and the image's corresponding dimension;
     *   <li>Non-<code>null</code> default tile size returned by <code>JAI.getDefaultTileSize()</code>, if the
     *       corresponding image dimension is at least double the default tile size;
     *   <li>The dimensions of the image itself;
     * </ol>
     *
     * <p>The <code>sources</code> contains a list of immediate sources of this image. Elements in the list may not be
     * <code>null</code>. If this image has no sources this argument should be <code>null</code>. This parameter is
     * forwarded unmodified to the <code>PlanarImage</code> constructor.
     *
     * <p>The <code>configuration</code> contains a mapping of configuration variables and image properties. Entries
     * which have keys of type <code>RenderingHints.Key</code> are taken to be configuration variables. Entries with a
     * key which is either a <code>String</code> or a <code>CaselessStringKey</code> are interpreted as image
     * properties. This parameter is forwarded unmodified to the <code>PlanarImage</code> constructor.
     *
     * <p>This image class recognizes the configuration variables referenced by the following keys:
     *
     * <ul>
     *   <li><code>JAI.KEY_TILE_CACHE</code>: specifies the <code>TileCache</code> in which to store the image tiles; if
     *       this key is not supplied no tile caching will be performed.
     *   <li><code>JAI.KEY_TILE_CACHE_METRIC</code>: establishes an ordering of tiles stored in the tile cache. This
     *       ordering is used to determine which tiles will be removed first, if a condition causes tiles to be removed
     *       from the cache.
     *   <li><code>JAI.KEY_TILE_SCHEDULER</code>: specifies the <code>TileScheduler</code> to use to schedule tile
     *       computation; if this key is not supplied the default scheduler will be used.
     *   <li><code>JAI.KEY_COLOR_MODEL_FACTORY</code>: specifies a <code>ColorModelFactory</code> to be used to generate
     *       the <code>ColorModel</code> of the image. If such a callback is provided it will be invoked if and only if
     *       either no <code>ImageLayout</code> hint is given, or an <code>ImageLayout</code> hint is given but contains
     *       a non-<code>null</code> <code>ColorModel</code> which is incompatible with the image <code>SampleModel
     *       </code>. In other words, such a callback provides the second priority mechanism for setting the <code>
     *       ColorModel</code> of the image.
     *   <li><code>JAI.KEY_DEFAULT_COLOR_MODEL_ENABLED</code>: specifies whether a default <code>ColorModel</code> will
     *       be derived if none is specified and one cannot be inherited from the first source; if this key is not
     *       supplied a default <code>ColorModel</code> will be computed if necessary.
     *   <li><code>JAI.KEY_DEFAULT_COLOR_MODEL_METHOD</code>: specifies the method to be used to compute the default
     *       <code>ColorModel</code>; if this key is not supplied and a default <code>ColorModel</code> is required,
     *       <code>PlanarImage.createColorModel()</code> will be used to compute it.
     *   <li><code>JAI.KEY_TILE_FACTORY</code>: specifies a {@link TileFactory} to be used to generate the tiles of the
     *       image via {@link TileFactory#createTile(SampleModel,Point)}. If no such configuration variable is given, a
     *       new <code>Raster</code> will be created for each image tile. This behavior may be overridden by subclasses
     *       which have alternate means of saving memory, for example as in the case of point operations which may
     *       overwrite a source image not referenced by user code. Note that the corresponding instance variable is
     *       actually set by the superclass constructor.
     *   <li><code>JAI.KEY_TILE_RECYCLER</code>: specifies a {@link TileRecycler} to be used to recycle the tiles of the
     *       image when the <code>dispose()</code> method is invoked. If such a configuration variable is set, the image
     *       has a non-<code>null</code> <code>TileCache</code>, and tile recycling is enabled, then invoking <code>
     *       dispose()</code> will cause each of the tiles of this image currently in the cache to be passed to the
     *       configured <code>TileRecycler</code> via {@link TileRecycler#recycleTile(Raster)}.
     *   <li><code>JAI.KEY_CACHED_TILE_RECYCLING_ENABLED</code>: specifies a <code>Boolean</code> value which indicates
     *       whether {#dispose()} should pass to <code>tileRecycler.recycleTile()</code> any image tiles remaining in
     *       the cache.
     * </ul>
     *
     * <p>The <code>cobbleSources</code> indicates which one of the two variants of the <code>computeRect</code> method
     * should be called. If a subclass does not follow the default tile computation scheme, then this argument may be
     * irrelevant.
     *
     * @param layout The layout of this image.
     * @param sources The immediate sources of this image.
     * @param configuration Configurable attributes of the image including configuration variables indexed by <code>
     *     RenderingHints.Key</code>s and image properties indexed by <code>String</code>s or <code>CaselessStringKey
     *     </code>s. This parameter may be <code>null</code>.
     * @param cobbleSources Indicates which variant of the <code>computeRect</code> method should be called.
     * @throws IllegalArgumentException If <code>sources</code> is non-<code>null</code> and any object in <code>sources
     *     </code> is <code>null</code>.
     * @throws RuntimeException If default <code>ColorModel</code> setting is enabled via a hint in the configuration
     *     <code>Map</code> and the supplied <code>Method</code> does not conform to the requirements stated in the
     *     <code>JAI</code> class for the hint key <code>KEY_DEFAULT_COLOR_MODEL_METHOD</code>.
     * @since JAI 1.1
     */
    public OpImage(Vector sources, ImageLayout layout, Map configuration, boolean cobbleSources) {
        super(layoutHelper(layout, sources, configuration), sources, configuration);

        if (configuration != null) {
            // Get the cache from the configuration map.
            Object cacheConfig = configuration.get(JAI.KEY_TILE_CACHE);

            // Ensure that it is a TileCache instance with positive capacity.
            if (cacheConfig != null
                    && cacheConfig instanceof TileCache
                    && ((TileCache) cacheConfig).getMemoryCapacity() > 0) {
                cache = (TileCache) cacheConfig;
            }

            // Get the scheduler from the configuration map.
            Object schedulerConfig = configuration.get(JAI.KEY_TILE_SCHEDULER);

            // Ensure that it is a TileScheduler instance.
            if (schedulerConfig != null && schedulerConfig instanceof TileScheduler) {
                scheduler = (TileScheduler) schedulerConfig;
            }

            try {
                // Test whether the TileScheduler is the default type.
                Class sunScheduler = Class.forName("org.eclipse.imagen.media.util.SunTileScheduler");

                isSunTileScheduler = sunScheduler.isInstance(scheduler);
            } catch (Exception e) {
                // Deliberately ignore any Exceptions.
            }

            // Get the tile metric (cost or priority, for example)
            tileCacheMetric = configuration.get(JAI.KEY_TILE_CACHE_METRIC);

            // Set up cached tile recycling flag.
            Object recyclingEnabledValue = configuration.get(JAI.KEY_CACHED_TILE_RECYCLING_ENABLED);
            if (recyclingEnabledValue instanceof Boolean) {
                isCachedTileRecyclingEnabled = ((Boolean) recyclingEnabledValue).booleanValue();
            }

            // Set up the TileRecycler.
            Object recyclerValue = configuration.get(JAI.KEY_TILE_RECYCLER);
            if (recyclerValue instanceof TileRecycler) {
                tileRecycler = (TileRecycler) recyclerValue;
            }
        }

        this.cobbleSources = cobbleSources;
    }

    /**
     * A <code>TileComputationListener</code> to pass to the <code>TileScheduler</code> to intercept method calls such
     * that the computed tiles are added to the <code>TileCache</code> of the image.
     */
    private class TCL implements TileComputationListener {
        OpImage opImage;

        private TCL(OpImage opImage) {
            this.opImage = opImage;
        }

        public void tileComputed(
                Object eventSource, TileRequest[] requests, PlanarImage image, int tileX, int tileY, Raster tile) {
            if (image == opImage) {
                // Cache the tile.
                addTileToCache(tileX, tileY, tile);
            }
        }

        public void tileCancelled(Object eventSource, TileRequest[] requests, PlanarImage image, int tileX, int tileY) {
            // Do nothing.
        }

        public void tileComputationFailure(
                Object eventSource,
                TileRequest[] requests,
                PlanarImage image,
                int tileX,
                int tileY,
                Throwable situation) {
            // Do nothing.
        }
    }

    /**
     * Stores a <code>RenderedImage</code> in a <code>Vector</code>.
     *
     * @param image The image to be stored in the <code>Vector</code>.
     * @return A <code>Vector</code> containing the image.
     * @throws IllegalArgumentException if <code>image</code> is <code>null</code>.
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image) {
        if (image == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(1);
        v.addElement(image);
        return v;
    }

    /**
     * Stores two <code>RenderedImage</code>s in a <code>Vector</code>.
     *
     * @param image1 The first image to be stored in the <code>Vector</code>.
     * @param image2 The second image to be stored in the <code>Vector</code>.
     * @return A <code>Vector</code> containing the images.
     * @throws IllegalArgumentException if <code>image1</code> or <code>image2</code> is <code>null</code>.
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image1, RenderedImage image2) {
        if (image1 == null || image2 == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(2);
        v.addElement(image1);
        v.addElement(image2);
        return v;
    }

    /**
     * Stores three <code>RenderedImage</code>s in a <code>Vector</code>.
     *
     * @param image1 The first image to be stored in the <code>Vector</code>.
     * @param image2 The second image to be stored in the <code>Vector</code>.
     * @param image3 The third image to be stored in the <code>Vector</code>.
     * @return A <code>Vector</code> containing the images.
     * @throws IllegalArgumentException if <code>image1</code> or <code>image2</code> or <code>image3</code> is <code>
     *     null</code>.
     * @since JAI 1.1
     */
    protected static Vector vectorize(RenderedImage image1, RenderedImage image2, RenderedImage image3) {
        if (image1 == null || image2 == null || image3 == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
        }
        Vector v = new Vector(3);
        v.addElement(image1);
        v.addElement(image2);
        v.addElement(image3);
        return v;
    }

    /**
     * Checks the source <code>Vector</code>.
     *
     * <p>Checks whether the <code>sources</code> parameter is <code>null</code> and optionally whether all elements are
     * non-<code>null</code>.
     *
     * @param sources The source <code>Vector</code>.
     * @param checkElements Whether the elements are to be checked.
     * @return The <code>sources</code> parameter unmodified.
     * @throws IllegalArgumentException If <code>sources</code> is <code>null</code>.
     * @throws IllegalArgumentException If <code>checkElements</code> is <code>true</code>, <code>sources</code> is non-
     *     <code>null</code> and any object in <code>sources</code> is <code>null</code>.
     */
    static Vector checkSourceVector(Vector sources, boolean checkElements) {
        // Check for null source Vector.
        if (sources == null) {
            throw new IllegalArgumentException(JaiI18N.getString("OpImage2"));
        }

        if (checkElements) {
            // Check Vector elements.
            int numSources = sources.size();
            for (int i = 0; i < numSources; i++) {
                // Check for null element.
                if (sources.get(i) == null) {
                    throw new IllegalArgumentException(JaiI18N.getString("OpImage3"));
                }
            }
        }

        return sources;
    }

    /**
     * Returns the tile cache object of this image by reference. If this image does not have a tile cache, this method
     * returns <code>null</code>.
     *
     * @since JAI 1.1
     */
    public TileCache getTileCache() {
        return cache;
    }

    /**
     * Sets the tile cache object of this image. A <code>null</code> input indicates that this image should have no tile
     * cache and subsequently computed tiles will not be cached.
     *
     * <p>The existing cache object is informed to release all the currently cached tiles of this image.
     *
     * @param cache A cache object to be used for caching this image's tiles, or <code>null</code> if no tile caching is
     *     desired.
     */
    public void setTileCache(TileCache cache) {
        if (this.cache != null) {
            this.cache.removeTiles(this);
        }
        this.cache = cache;
    }

    /**
     * Retrieves a tile from the tile cache. If this image does not have a tile cache, or the requested tile is not
     * currently in the cache, this method returns <code>null</code>.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     * @return The requested tile as a <code>Raster</code> or <code>null</code>.
     */
    protected Raster getTileFromCache(int tileX, int tileY) {
        return cache != null ? cache.getTile(this, tileX, tileY) : null;
    }

    /**
     * Adds a tile to the tile cache. If this image does not have a tile cache, this method does nothing.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     * @param tile The tile to be added to the cache.
     */
    protected void addTileToCache(int tileX, int tileY, Raster tile) {
        if (cache != null) {
            cache.add(this, tileX, tileY, tile, tileCacheMetric);
        }
    }

    /**
     * Returns the <code>tileCacheMetric</code> instance variable by reference.
     *
     * @since JAI 1.1
     */
    public Object getTileCacheMetric() {
        return tileCacheMetric;
    }

    /**
     * Returns a tile of this image as a <code>Raster</code>. If the requested tile is completely outside of this
     * image's bounds, this method returns <code>null</code>.
     *
     * <p>This method attempts to retrieve the requested tile from the cache. If the tile is not currently in the cache,
     * it schedules the tile for computation and adds it to the cache once the tile has been computed.
     *
     * <p>If a subclass overrides this method, then it needs to handle tile caching and scheduling. It should also
     * override <code>computeTile()</code> which may be invoked directly by the <code>TileScheduler</code>.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     */
    public Raster getTile(int tileX, int tileY) {
        Raster tile = null; // the requested tile, to be returned

        // Make sure the requested tile is inside this image's boundary.
        if (tileX >= getMinTileX() && tileX <= getMaxTileX() && tileY >= getMinTileY() && tileY <= getMaxTileY()) {
            // Check if tile is available in the cache.
            tile = getTileFromCache(tileX, tileY);

            if (tile == null) { // tile not in cache
                try {
                    tile = scheduler.scheduleTile(this, tileX, tileY);
                } catch (OutOfMemoryError e) {
                    // Empty the cache and call System.gc()
                    if (cache != null) {
                        cache.flush();
                        System.gc(); // slow
                    }

                    // Need to reissue the tile scheduling.
                    tile = scheduler.scheduleTile(this, tileX, tileY);
                }

                // Cache the result tile.
                addTileToCache(tileX, tileY, tile);
            }
        }

        return tile;
    }

    /**
     * Computes the image data of a tile.
     *
     * <p>When a tile is requested via the <code>getTile</code> method and that tile is not in this image's tile cache,
     * this method is invoked by the <code>TileScheduler</code> to compute the data of the new tile. Even though this
     * method is marked <code>public</code>, it should not be called by the applications directly. Rather, it is meant
     * to be called by the <code>TileScheduler</code> for the actual computation.
     *
     * <p>The implementation of this method in this class assumes that the requested tile either intersects the image,
     * or is within the image's bounds. It creates a new <code>Raster</code> to represent the requested tile, then calls
     * one of the two variants of <code>computeRect</code> to calculate the pixels of the tile that are within the
     * image's bounds. The value of <code>cobbleSources</code> determines which variant of <code>computeRect</code> is
     * invoked, as described in the class comments.
     *
     * <p>Subclasses may provide a more optimized implementation of this method. If they override this method not to
     * call either variant of <code>computeRect</code>, then neither variant of <code>computeRect</code> needs to be
     * implemented.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     */
    public Raster computeTile(int tileX, int tileY) {
        // Create a new Raster.
        WritableRaster dest = createWritableRaster(sampleModel, new Point(tileXToX(tileX), tileYToY(tileY)));

        // Determine the active area; tile intersects with image's bounds.
        Rectangle destRect = getTileRect(tileX, tileY);

        int numSources = getNumSources();

        if (cobbleSources) {
            Raster[] rasterSources = new Raster[numSources];
            // Cobble areas
            for (int i = 0; i < numSources; i++) {
                PlanarImage source = getSource(i);
                Rectangle srcRect = mapDestRect(destRect, i);

                // If srcRect is empty, set the Raster for this source to
                // null; otherwise pass srcRect to getData(). If srcRect
                // is null, getData() will return a Raster containing the
                // data of the entire source image.
                rasterSources[i] = srcRect != null && srcRect.isEmpty() ? null : source.getData(srcRect);
            }
            computeRect(rasterSources, dest, destRect);

            for (int i = 0; i < numSources; i++) {
                Raster sourceData = rasterSources[i];
                if (sourceData != null) {
                    PlanarImage source = getSourceImage(i);

                    // Recycle the source tile
                    if (source.overlapsMultipleTiles(sourceData.getBounds())) {
                        recycleTile(sourceData);
                    }
                }
            }
        } else {
            PlanarImage[] imageSources = new PlanarImage[numSources];
            for (int i = 0; i < numSources; i++) {
                imageSources[i] = getSource(i);
            }
            computeRect(imageSources, dest, destRect);
        }

        return dest;
    }

    /**
     * Computes a rectangle of output, given <code>Raster</code> sources. This method should be overridden by <code>
     * OpImage</code> subclasses that make use of cobbled sources, as determined by the setting of the <code>
     * cobbleSources</code> constructor argument to this class.
     *
     * <p>The source <code>Raster</code>s are guaranteed to include at least the area specified by <code>
     * mapDestRect(destRect)</code> unless this area is empty or does not intersect the corresponding source in which
     * case the source <code>Raster</code> will be <code>null</code>. Only the specified destination region should be
     * written.
     *
     * <p>Since the subclasses of <code>OpImage</code> may choose between the cobbling and non-cobbling versions of
     * <code>computeRect</code>, it is not possible to leave this method abstract in <code>OpImage</code>. Instead, a
     * default implementation is provided that throws a <code>RuntimeException</code>.
     *
     * @param sources an array of source <code>Raster</code>s, one per source image.
     * @param dest a <code>WritableRaster</code> to be filled in.
     * @param destRect the <code>Rectangle</code> within the destination to be written.
     * @throws RuntimeException If this method is invoked on the subclass that sets <code>cobbleSources</code> to <code>
     *     true</code> but does not supply an implementation of this method.
     */
    protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) {
        String className = this.getClass().getName();
        throw new RuntimeException(className + " " + JaiI18N.getString("OpImage0"));
    }

    /**
     * Computes a rectangle of output, given <code>PlanarImage</code> sources. This method should be overridden by
     * <code>OpImage</code> subclasses that do not require cobbled sources; typically they will instantiate iterators to
     * perform source access, but they may access sources directly (via the <code>SampleModel</code>/<code>DataBuffer
     * </code> interfaces) if they wish.
     *
     * <p>Since the subclasses of <code>OpImage</code> may choose between the cobbling and non-cobbling versions of
     * <code>computeRect</code>, it is not possible to leave this method abstract in <code>OpImage</code>. Instead, a
     * default implementation is provided that throws a <code>RuntimeException</code>.
     *
     * @param sources an array of <code>PlanarImage</code> sources.
     * @param dest a <code>WritableRaster</code> to be filled in.
     * @param destRect the <code>Rectangle</code> within the destination to be written.
     * @throws RuntimeException If this method is invoked on the subclass that sets <code>cobbleSources</code> to <code>
     *     false</code> but does not supply an implementation of this method.
     */
    protected void computeRect(PlanarImage[] sources, WritableRaster dest, Rectangle destRect) {
        String className = this.getClass().getName();
        throw new RuntimeException(className + " " + JaiI18N.getString("OpImage1"));
    }

    /**
     * Returns a list of indices of the tiles of a given source image that may be required in order to compute a given
     * tile. Ideally, only tiles that will be requested by means of calls to the source's <code>getTile()</code> method
     * should be reported. The default implementation uses <code>mapDestRect()</code> to obtain a conservative estimate.
     *
     * <p>If no dependencies exist, this method returns <code>null</code>.
     *
     * <p>This method may be used by optimized implementations of JAI in order to predict future work and create an
     * optimized schedule for performing it.
     *
     * <p>A given <code>OpImage</code> may mix calls to <code>getTile()</code> with calls to other methods such as
     * <code>getData()</code> and <code>copyData()</code> in order to avoid requesting entire tiles where only a small
     * portion is needed. In such a case, this method may be overridden to provide a more accurate estimate of the set
     * of <code>getTile()</code> calls that will actually be performed.
     *
     * @param tileX the X index of the tile.
     * @param tileY the Y index of the tile.
     * @param sourceIndex the index of the source image.
     * @return An array of <code>Point</code>s indicating the source tile dependencies.
     * @throws IllegalArgumentException If <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     */
    public Point[] getTileDependencies(int tileX, int tileY, int sourceIndex) {
        if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            // Specified source does not exist for this image.
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        Rectangle rect = getTileRect(tileX, tileY);
        if (rect.isEmpty()) {
            // The tile is outside of the image bounds.
            return null;
        }

        // Returns a list of tiles that belong to the source specified by
        // the <code>sourceIndex</code> argument, which are need to compute
        // the pixels within the rectangle region specified by the
        // <code>rect</code> argument of this image.
        //
        // This method uses <code>mapDestRect</code> to conservatively
        // determine the source region required.  However, only those tiles
        // actually inside the source image bound are returned.  If the
        // region of interest maps completely outside of the source image,
        // <code>null</code> is returned.
        PlanarImage src = getSource(sourceIndex);
        Rectangle srcRect = mapDestRect(rect, sourceIndex);

        int minTileX = src.XToTileX(srcRect.x);
        int maxTileX = src.XToTileX(srcRect.x + srcRect.width - 1);

        int minTileY = src.YToTileY(srcRect.y);
        int maxTileY = src.YToTileY(srcRect.y + srcRect.height - 1);

        // Make sure the tiles are really inside the source image.
        minTileX = Math.max(minTileX, src.getMinTileX());
        maxTileX = Math.min(maxTileX, src.getMaxTileX());

        minTileY = Math.max(minTileY, src.getMinTileY());
        maxTileY = Math.min(maxTileY, src.getMaxTileY());

        int numXTiles = maxTileX - minTileX + 1;
        int numYTiles = maxTileY - minTileY + 1;
        if (numXTiles <= 0 || numYTiles <= 0) {
            // The tile maps outside of source image bound.
            return null;
        }

        Point[] ret = new Point[numYTiles * numXTiles];
        int i = 0;

        for (int y = minTileY; y <= maxTileY; y++) {
            for (int x = minTileX; x <= maxTileX; x++) {
                ret[i++] = new Point(x, y);
            }
        }

        return ret;
    }

    /**
     * Computes the tiles indicated by the given tile indices. This call is preferable to a series of <code>getTile()
     * </code> calls because certain implementations can make optimizations based on the knowledge that multiple tiles
     * are being asked for at once.
     *
     * <p>The implementation of this method in this class uses multiple threads to compute multiple tiles at a time.
     *
     * @param tileIndices An array of <code>Point</code>s representing tile indices.
     * @return An array of <code>Raster</code>s containing the tiles corresponding to the given tile indices.
     * @throws IllegalArgumentException If <code>tileIndices</code> is <code>null</code>.
     */
    public Raster[] getTiles(Point[] tileIndices) {
        if (tileIndices == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        int numTiles = tileIndices.length; // number of tiles requested

        // The requested tiles, to be returned.
        Raster[] tiles = new Raster[numTiles];

        // Indicator for those tiles that actually need to be computed.
        boolean[] computeTiles = new boolean[numTiles];

        int minTileX = getMinTileX();
        int maxTileX = getMaxTileX();
        int minTileY = getMinTileY();
        int maxTileY = getMaxTileY();

        int count = 0; // number of tiles need to be computed

        for (int i = 0; i < numTiles; i++) {
            int tileX = tileIndices[i].x;
            int tileY = tileIndices[i].y;

            // Make sure the tile is inside image boundary.
            if (tileX >= minTileX && tileX <= maxTileX && tileY >= minTileY && tileY <= maxTileY) {
                // Check if tile is available in the cache.
                tiles[i] = getTileFromCache(tileX, tileY);

                if (tiles[i] == null) {
                    // Tile not in cache. needs computation.
                    computeTiles[i] = true;
                    count++;
                }
            }
        }

        if (count > 0) { // need to compute some tiles
            if (count == numTiles) {
                // None of the tiles is in cache.
                tiles = scheduler.scheduleTiles(this, tileIndices);

                if (cache != null) { // cache these tiles
                    if (cache != null) {
                        for (int i = 0; i < numTiles; i++) {
                            cache.add(this, tileIndices[i].x, tileIndices[i].y, tiles[i], tileCacheMetric);
                        }
                    }
                }

            } else {
                // Only schedule those tiles not in cache for computation.
                Point[] indices = new Point[count];
                count = 0;
                for (int i = 0; i < numTiles; i++) {
                    if (computeTiles[i]) {
                        indices[count++] = tileIndices[i];
                    }
                }

                // Schedule needed tiles and return.
                Raster[] newTiles = scheduler.scheduleTiles(this, indices);

                count = 0;
                for (int i = 0; i < numTiles; i++) {
                    if (computeTiles[i]) {
                        tiles[i] = newTiles[count++];
                        addTileToCache(tileIndices[i].x, tileIndices[i].y, tiles[i]);
                    }
                }
            }
        }

        return tiles;
    }

    private static TileComputationListener[] prependListener(
            TileComputationListener[] listeners, TileComputationListener listener) {
        if (listeners == null) {
            return new TileComputationListener[] {listener};
        }

        TileComputationListener[] newListeners = new TileComputationListener[listeners.length + 1];
        newListeners[0] = listener;
        System.arraycopy(listeners, 0, newListeners, 1, listeners.length);

        return newListeners;
    }

    /** Returns an array of indices of tiles which are not cached or <code>null</code> if all are cached. */
    /* XXX
    private Point[] pruneIndices(Point[] tileIndices) {
        if(true)return tileIndices;//XXX
        int numIndices = tileIndices.length;

        ArrayList uncachedIndices = new ArrayList(numIndices);

        for(int i = 0; i < numIndices; i++) {
            Point p = tileIndices[i];
            if(getTileFromCache(p.x, p.y) == null) {
                uncachedIndices.add(p);
            }
        }

        int numUncached = uncachedIndices.size();
        return numUncached > 0 ?
            (Point[])uncachedIndices.toArray(new Point[numUncached]) : null;
    }
    */

    /**
     * Queues a list of tiles for computation. Registered listeners will be notified after each tile has been computed.
     * The event source parameter passed to such listeners will be the <code>TileScheduler</code> and the image
     * parameter will be this image.
     *
     * @param tileIndices A list of tile indices indicating which tiles to schedule for computation.
     * @throws IllegalArgumentException If <code>tileIndices</code> is <code>null</code>.
     * @since JAI 1.1
     */
    public TileRequest queueTiles(Point[] tileIndices) {
        if (tileIndices == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        /* XXX bad idea probably
        // Remove any tile indices corresponding to cached tiles.
        tileIndices = pruneIndices(tileIndices);

        // Return if no tiles remain, i.e., all are cached.
        if(tileIndices == null) {
            return;
        }
        */

        // Get registered listeners.
        TileComputationListener[] tileListeners = getTileComputationListeners();

        // Add a listener to cache tiles only if not a SunTileScheduler.
        // The SunTileScheduler caches tiles generated by an OpImage but
        // this is not a requirement of the specification.
        if (!isSunTileScheduler) {
            // Create a local listener.
            TileComputationListener localListener = new TCL(this);

            // Prepend local listener to array.
            tileListeners = prependListener(tileListeners, localListener);
        }

        // Queue the tiles to the scheduler.
        return scheduler.scheduleTiles(this, tileIndices, tileListeners);
    }

    /**
     * Issue an advisory cancellation request to nullify processing of the indicated tiles via the TileScheduler for
     * this image. This method should merely forward the request to the associated <code>TileScheduler</code>.
     *
     * @param request The request for which tiles are to be cancelled.
     * @param tileIndices The tiles to be cancelled; may be <code>null</code>. Any tiles not actually in the <code>
     *     TileRequest</code> will be ignored.
     * @throws IllegalArgumentException If <code>request</code> is <code>null</code>.
     * @since JAI 1.1
     */
    public void cancelTiles(TileRequest request, Point[] tileIndices) {
        if (request == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic4"));
        }
        scheduler.cancelTiles(request, tileIndices);
    }

    /**
     * Hints that the given tiles might be needed in the near future. Some implementations may spawn one or more threads
     * to compute the tiles, while others may ignore the hint.
     *
     * @param tileIndices A list of tile indices indicating which tiles to prefetch.
     * @throws IllegalArgumentException If <code>tileIndices</code> is <code>null</code>.
     */
    public void prefetchTiles(Point[] tileIndices) {
        if (tileIndices == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        /* XXX bad idea probably
        // Remove any tile indices corresponding to cached tiles.
        tileIndices = pruneIndices(tileIndices);
        */

        // Return if no tiles remain, i.e., all are cached.
        if (tileIndices == null) {
            return;
        }

        // Prefetch any remaining tiles.
        scheduler.prefetchTiles(this, tileIndices);
    }

    /**
     * Computes the position in the specified source that best matches the supplied destination image position. If it is
     * not possible to compute the requested position, <code>null</code> will be returned. If the point is mapped
     * outside the source bounds, the coordinate value or <code>null</code> may be returned at the discretion of the
     * implementation.
     *
     * <p>Floating-point input and output coordinates are supported, and recommended when possible. Subclass
     * implementations may however use integer computation if necessary for simplicity.
     *
     * <p>The implementation in this class returns the value of <code>pt</code> in the following code snippet:
     *
     * <pre>
     * Rectangle destRect = new Rectangle((int)destPt.getX(),
     *                                    (int)destPt.getY(),
     *                                    1, 1);
     * Rectangle sourceRect = mapDestRect(destRect, sourceIndex);
     * Point2D pt = (Point2D)destPt.clone();
     * pt.setLocation(sourceRect.x + (sourceRect.width - 1.0)/2.0,
     *                sourceRect.y + (sourceRect.height - 1.0)/2.0);
     * </pre>
     *
     * Subclasses requiring different behavior should override this method.
     *
     * @param destPt the position in destination image coordinates to map to source image coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Point2D</code> of the same class as <code>destPt</code> or <code>null</code>.
     * @throws IllegalArgumentException if <code>destPt</code> is <code>null</code>.
     * @throws IndexOutOfBoundsException if <code>sourceIndex</code> is negative or greater than or equal to the number
     *     of sources.
     * @since JAI 1.1.2
     */
    public Point2D mapDestPoint(Point2D destPt, int sourceIndex) {
        if (destPt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        } else if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IndexOutOfBoundsException(JaiI18N.getString("Generic1"));
        }

        Rectangle destRect = new Rectangle((int) destPt.getX(), (int) destPt.getY(), 1, 1);

        Rectangle sourceRect = mapDestRect(destRect, sourceIndex);

        Point2D pt = (Point2D) destPt.clone();
        pt.setLocation(sourceRect.x + (sourceRect.width - 1.0) / 2.0, sourceRect.y + (sourceRect.height - 1.0) / 2.0);

        return pt;
    }

    /**
     * Computes the position in the destination that best matches the supplied source image position. If it is not
     * possible to compute the requested position, <code>null</code> will be returned. If the point is mapped outside
     * the destination bounds, the coordinate value or <code>null</code> may be returned at the discretion of the
     * implementation.
     *
     * <p>Floating-point input and output coordinates are supported, and recommended when possible. Subclass
     * implementations may however use integer computation if necessary for simplicity.
     *
     * <p>The implementation in this class returns the value of <code>pt</code> in the following code snippet:
     *
     * <pre>
     * Rectangle sourceRect = new Rectangle((int)sourcePt.getX(),
     *                                      (int)sourcePt.getY(),
     *                                      1, 1);
     * Rectangle destRect = mapSourceRect(sourceRect, sourceIndex);
     * Point2D pt = (Point2D)sourcePt.clone();
     * pt.setLocation(destRect.x + (destRect.width - 1.0)/2.0,
     *                destRect.y + (destRect.height - 1.0)/2.0);
     * </pre>
     *
     * @param sourcePt the position in source image coordinates to map to destination image coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Point2D</code> of the same class as <code>sourcePt</code> or <code>null</code>.
     * @throws IllegalArgumentException if <code>sourcePt</code> is <code>null</code>.
     * @throws IndexOutOfBoundsException if <code>sourceIndex</code> is negative or greater than or equal to the number
     *     of sources.
     * @since JAI 1.1.2
     */
    public Point2D mapSourcePoint(Point2D sourcePt, int sourceIndex) {
        if (sourcePt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        } else if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IndexOutOfBoundsException(JaiI18N.getString("Generic1"));
        }

        Rectangle sourceRect = new Rectangle((int) sourcePt.getX(), (int) sourcePt.getY(), 1, 1);

        Rectangle destRect = mapSourceRect(sourceRect, sourceIndex);

        // Return null of destination rectangle is not computable.
        if (destRect == null) {
            return null;
        }

        Point2D pt = (Point2D) sourcePt.clone();
        pt.setLocation(destRect.x + (destRect.width - 1.0) / 2.0, destRect.y + (destRect.height - 1.0) / 2.0);

        return pt;
    }

    /**
     * Returns a conservative estimate of the destination region that can potentially be affected by the pixels of a
     * rectangle of a given source. An empty <code>Rectangle</code> may be returned if the destination is unaffected by
     * the contents of the source rectangle. This is distinct from a <code>null</code> return value which serves rather
     * to indicate that it is not possible to determine the bounds of the affected region. The safest interpretation of
     * a <code>null</code> return value is that the entire destination might be affected by any pixel within the given
     * source rectangle.
     *
     * @param sourceRect The <code>Rectangle</code> in source coordinates.
     * @param sourceIndex The index of the source image.
     * @return A <code>Rectangle</code> indicating the potentially affected destination region, or <code>null</code> if
     *     the region is unknown.
     * @throws IllegalArgumentException If the source index is negative or greater than that of the last source.
     * @throws IllegalArgumentException If <code>sourceRect</code> is <code>null</code>.
     */
    public abstract Rectangle mapSourceRect(Rectangle sourceRect, int sourceIndex);

    /**
     * Returns a conservative estimate of the region of a specified source that is required in order to compute the
     * pixels of a given destination rectangle. The computation may as appropriate clip the mapped <code>Rectangle
     * </code> to the actual bounds of the source or may treat the source as having infinite extent. It is therefore the
     * responsibility of the invoking object to constrain the region in accordance with its needs. Returning an empty
     * <code>Rectangle</code> should indicate that the data of the source image in question are not required for the
     * computation of the specified destination region. If the entire source image might be required to compute this
     * destination region, then <code>getSourceImage(sourceIndex).getBounds()</code> should be returned.
     *
     * <p>To illustrate the issue of whether the source should be thought to have infinite extent, consider the case
     * wherein computing a destination pixel requires multiple source pixels of context. At the source image boundary,
     * these pixels might only be available if the source data were extrapolated, e.g., using a {@link BorderExtender}.
     * If such an extender were available, destination pixels could be computed even if they mapped to a region on the
     * source boundary so in this case the source could be considered to have infinite extent. If no such extender were
     * available, only destination pixels with source context contained within the source image bounds could be
     * considered so that it might be preferable to clip the rectangle to the source bounds.
     *
     * @param destRect The <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex The index of the source image.
     * @return A non-<code>null</code> <code>Rectangle</code> indicating the required source region.
     * @throws IllegalArgumentException If the source index is negative or greater than that of the last source.
     * @throws IllegalArgumentException If <code>destRect</code> is <code>null</code>.
     */
    public abstract Rectangle mapDestRect(Rectangle destRect, int sourceIndex);

    /**
     * Returns one of <code>OP_COMPUTE_BOUND</code>, <code>OP_IO_BOUND</code>, or <code>OP_NETWORK_BOUND</code> to
     * indicate how the operation is likely to spend its time. The answer does not affect the output of the operation,
     * but may allow a scheduler to parallelize the computation of multiple operations more effectively.
     *
     * <p>The implementation of this method in this class returns <code>OP_COMPUTE_BOUND</code>.
     */
    public int getOperationComputeType() {
        return OP_COMPUTE_BOUND;
    }

    /**
     * Returns <code>true</code> if the <code>OpImage</code> returns an unique <code>Raster</code> object every time
     * <code>computeTile</code> is called. <code>OpImage</code>s that internally cache <code>Raster</code>s and return
     * them via <code>computeTile</code> should return <code>false</code> for this method.
     *
     * <p>The implementation of this method in this class always returns <code>true</code>.
     */
    public boolean computesUniqueTiles() {
        return true;
    }

    /**
     * Uncaches all tiles and calls <code>super.dispose()</code>. If a <code>TileRecycler</code> was defined via the
     * configuration variable <code>JAI.KEY_TILE_RECYCLER</code> when this image was constructed and tile recycling was
     * enabled via the configuration variable <code>JAI.KEY_CACHED_TILE_RECYCLING_ENABLED</code>, then each of this
     * image's tiles which is currently in the cache will be recycled. This method may be invoked more than once
     * although invocations after the first one may do nothing.
     *
     * <p>The results of referencing an image after a call to <code>dispose()</code> are undefined.
     *
     * @since JAI 1.1.2
     */
    public synchronized void dispose() {
        if (isDisposed) {
            return;
        }

        isDisposed = true;

        if (cache != null) {
            if (isCachedTileRecyclingEnabled && tileRecycler != null) {
                Raster[] tiles = cache.getTiles(this);
                if (tiles != null) {
                    int numTiles = tiles.length;
                    for (int i = 0; i < numTiles; i++) {
                        tileRecycler.recycleTile(tiles[i]);
                    }
                }
            }
            cache.removeTiles(this);
        }
        super.dispose();
    }

    /**
     * Indicates whether the source with the given index has a <code>BorderExtender</code>. If the source index is out
     * of bounds for the source vector of this <code>OpImage</code> then an <code>ArrayIndexOutOfBoundsException</code>
     * may be thrown.
     *
     * @param sourceIndex The index of the source in question.
     * @return <code>true</code> if the indicated source has an extender.
     * @deprecated as of JAI 1.1.
     */
    public boolean hasExtender(int sourceIndex) {
        if (sourceIndex != 0) {
            throw new ArrayIndexOutOfBoundsException();
        } else if (this instanceof AreaOpImage) {
            return ((AreaOpImage) this).getBorderExtender() != null;
        } else if (this instanceof GeometricOpImage) {
            return ((GeometricOpImage) this).getBorderExtender() != null;
        }
        return false;
    }

    /**
     * Returns the effective number of bands of an image with a given <code>SampleModel</code> and <code>ColorModel
     * </code>. Normally, this is given by <code>sampleModel.getNumBands()</code>, but for images with an <code>
     * IndexColorModel</code> the effective number of bands is given by <code>colorModel.getNumComponents()</code>,
     * since a single physical sample represents multiple color components.
     *
     * @deprecated as of JAI 1.1.
     */
    public static int getExpandedNumBands(SampleModel sampleModel, ColorModel colorModel) {
        if (colorModel instanceof IndexColorModel) {
            return colorModel.getNumComponents();
        } else {
            return sampleModel.getNumBands();
        }
    }

    /**
     * Returns the image's format tags to be used with a <code>RasterAccessor</code>.
     *
     * <p>This method will compute and cache the tags the first time it is called on a particular image. The image's
     * <code>SampleModel</code> and <code>ColorModel</code> must be set to their final values before calling this
     * method.
     *
     * @return An array containing <code>RasterFormatTag</code>s for the sources in the first <code>getNumSources()
     *     </code> elements and a <code>RasterFormatTag</code> for the destination in the last element.
     */
    // XXX This method should be removed if we stop using RasterAccessor.
    protected synchronized RasterFormatTag[] getFormatTags() {
        if (formatTags == null) {
            RenderedImage[] sourceArray = new RenderedImage[getNumSources()];
            if (sourceArray.length > 0) {
                getSources().toArray(sourceArray);
            }
            formatTags = RasterAccessor.findCompatibleTags(sourceArray, this);
        }

        return formatTags;
    }

    /**
     * Returns the value of the instance variable <code>tileRecycler</code>.
     *
     * @since JAI 1.1.2
     */
    public TileRecycler getTileRecycler() {
        return tileRecycler;
    }

    /**
     * Creates a <code>WritableRaster</code> at the given tile grid position. The superclass method
     * {@link #createWritableRaster(SampleModel,Point)} will be invoked with this image's <code>SampleModel</code> and
     * the location of the specified tile.
     *
     * <p>Subclasses should ideally use this method to create destination tiles as this method will take advantage of
     * any <code>TileFactory</code> specified to the <code>OpImage</code> at construction.
     *
     * @since JAI 1.1.2
     */
    protected final WritableRaster createTile(int tileX, int tileY) {
        return createWritableRaster(sampleModel, new Point(tileXToX(tileX), tileYToY(tileY)));
    }

    /**
     * A tile recycling convenience method.
     *
     * <p>If <code>tileRecycler</code> is non-<code>null</code>, the call is forwarded to
     * {@link TileRecycler.recycleTile(Raster)}; otherwise the method does nothing.
     *
     * <p>This method is for use by subclasses which create <code>Raster</code>s with limited scope which therefore may
     * easily be identified as safe candidates for recycling. This might occur for example within
     * {@link #computeRect(Raster[],WritableRaster,Rectangle)} or {@link #computeTile(int,int)} wherein <code>Raster
     * </code>s may be created for use within the method but be eligible for garbage collection once the method is
     * exited.
     *
     * @throws IllegalArgumentException if <code>tile</code> is <code>null</code>.
     * @since JAI 1.1.2
     */
    protected void recycleTile(Raster tile) {
        if (tile == null) throw new IllegalArgumentException(JaiI18N.getString("Generic0"));

        if (tileRecycler != null) {
            tileRecycler.recycleTile(tile);
        }
    }
}
